﻿#region Copyright & License
/*
   Copyright 2009-2010 Stepan Adamec (adamec@yasas.org)

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License. 
 */
#endregion

namespace See.Sharper {
  using System;
  using System.IO;
  using System.Net;
  using System.Security.Cryptography;
  using System.Text;
  using System.Text.RegularExpressions;

  /// <summary>
  /// Extensions of frequently used classes in 'System' namespace.
  /// </summary>
  public static class Sharper_System {
    #region Array extensions

    /// <summary>
    /// Returns the first element of the array <paramref name="self"/>.
    /// </summary>
    /// <param name="self">The source array.</param>
    /// <returns>The value at the first position in the source array.</returns>
    public static object First(this Array self) {
      if (self.Length > 0) {
        return self.GetValue(0);
      }

      return null;
    }

    /// <summary>
    /// Returns the first element of the array <paramref name="self"/> using explicit type-cast.
    /// </summary>
    /// <typeparam name="T">Type used in type-cast.</typeparam>
    /// <param name="self">The source array.</param>
    /// <returns>The value at the first position in the source array.</returns>
    /// <see cref="See.Sharper.Sharper_System.First(Array)"/>
    public static T First<T>(this Array self) {
      return (T)First(self);
    }

    /// <summary>
    /// Test whether the given index is in bounds of the array <paramref name="self"/>.
    /// </summary>
    /// <param name="self">The source array.</param>
    /// <param name="index">Index of type <code>int</code> to be tested.</param>
    /// <returns></returns>
    public static bool IndexInBounds(this Array self, int index) {
      return ((index >= 0) && (index < self.Length));
    }

    /// <summary>
    /// Test whether the given index is in bounds of the array <paramref name="self"/>.
    /// </summary>
    /// <param name="self">The source array.</param>
    /// <param name="index">Index of type <code>long</code> to be tested.</param>
    /// <returns></returns>
    public static bool IndexInBounds(this Array self, long index) {
      return ((index >= 0) && (index < self.LongLength));
    }

    /// <summary>
    /// Returns the last element of the array <paramref name="self"/>.
    /// </summary>
    /// <param name="self">The source array.</param>
    /// <returns>The value at the last position in the source array.</returns>
    public static object Last(this Array self) {
      if (self.Length > 0) {
        return self.GetValue(self.Length - 1);
      }

      return null;
    }

    /// <summary>
    /// Returns the last element of the array <paramref name="self"/> using explicit type-cast.
    /// </summary>
    /// <typeparam name="T">Type used in type-cast.</typeparam>
    /// <param name="self">The source array.</param>
    /// <returns>The value at the last position in the source array.</returns>
    /// <see cref="See.Sharper.Sharper_System.First(Array)"/>
    public static T Last<T>(this Array self) {
      return (T)Last(self);
    }

    #endregion

    #region Boolean extensions

    /// <summary>
    /// Logical conjunction of two boolean operands.
    /// </summary>
    /// <param name="self">Left operand.</param>
    /// <param name="right">Right operand.</param>
    /// <returns>Result of logical conjunction.</returns>
    public static Boolean And(this Boolean self, Boolean right) {
      return (self & right);
    }

    /// <summary>
    /// Logical disjunction of two boolean operands.
    /// </summary>
    /// <param name="self">Left operand.</param>
    /// <param name="right">Right operand.</param>
    /// <returns>Result of logical disjunction.</returns>
    public static Boolean Or(this Boolean self, Boolean right) {
      return (self | right);
    }

    /// <summary>
    /// Exclusive disjunction of two boolean operands.
    /// </summary>
    /// <param name="self">Left operand.</param>
    /// <param name="right">Right operand.</param>
    /// <returns>Result of exclusive disjunction.</returns>
    public static Boolean Xor(this Boolean self, Boolean right) {
      return (self ^ right);
    }

    #endregion

    #region IDisposable extensions

    /// <summary>
    /// Executes the action in the scope of a disposable object <paramref name="self"/> at the end of which the object will be disposed.
    /// </summary>
    /// <typeparam name="I">Type of a disposable object.</typeparam>
    /// <param name="self">Disposable object.</param>
    /// <param name="action">Action to be executed.</param>
    public static void Using<I>(this I self, Action<I> action) where I : IDisposable {
      using (self) {
        action(self);
      }
    }

    #endregion

    #region Object extensions
    
    /// <summary>
    /// Executes the action in the scope of an object <paramref name="self"/>.
    /// </summary>
    /// <typeparam name="I">Type of an object.</typeparam>
    /// <param name="self">Object.</param>
    /// <param name="action">Action to be executed.</param>
    public static void With<I>(this I self, Action<I> action) {
      action(self);
    }
    
    #endregion
  
    #region Number extensions

    /// <summary>
    /// Tells whether the given number <paramref name="self"/> is even.
    /// </summary>
    /// <param name="self">The number.</param>
    /// <returns>True is the number is even.</returns>
    public static bool IsEven(this Int32 self) {
      return ((self & 2) == 0);
    }

    /// <summary>
    /// Tells whether the given number <paramref name="self"/> is odd.
    /// </summary>
    /// <param name="self">The number.</param>
    /// <returns>True is the number is odd.</returns>
    public static bool IsOdd(this Int32 self) {
      return ((self & 2) != 0);
    }

    /// <summary>
    /// Executes the <paramref name="action"/> many times, starting from zero. The current index is passed to the action each time.
    /// <example><code>
    /// 32.Times(
    ///   i => System.Console.WriteLine(i)
    /// );
    /// </code></example>
    /// </summary>
    /// <typeparam name="Int32">Type that this extension method will be assigned to.</typeparam>
    /// <param name="self">Integer value.</param>
    /// <param name="action">Action to be executed.</param>
    public static void Times<Int32>(this Int32 self, Action<int> action) {
      for (int i = 0; i < Convert.ToInt32(self); i++) {
        action(i);
      }
    }
    
    #endregion
  
    #region String extensions

    /// <summary>
    /// Decrypts the Base64 encoded string value using Rijndael algorithm.
    /// <example><code>
    /// System.Console.WriteLine(
    ///   "EfOSkt1p2yRO/TLxMJBcs/DeAa3MpMqkA1TMqZW/75czfOgGPqmTf6Nt1MsgRVHG".Decrypt("secret")
    /// );
    /// Output: Text to be encrypted.
    /// </code></example>
    /// </summary>
    /// <param name="self">Base64 encoded string to be decrypted.</param>
    /// <param name="password">Password used for decryption.</param>
    /// <returns>Raw decrypted text.</returns>
    public static string Decrypt(this string self, string password) {
      new RijndaelManaged().Using(cipher => {
        PasswordDeriveBytes key = CreateDerivedKey(password);

        cipher.CreateDecryptor(key.GetBytes(32), key.GetBytes(16)).Using(decryptor => {
          byte[] raw = Convert.FromBase64String(self);
          byte[] decryptedBytes = new byte[raw.Length];
          int count = 0;

          new MemoryStream(raw).Using(memoryStream => {
            try {
              new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read).Using(cryptoStream => {
                try {
                  count = cryptoStream.Read(decryptedBytes, 0, decryptedBytes.Length);
                } finally {
                  cryptoStream.Close();
                }
              });
            } finally {
              memoryStream.Close();
            }
          });
          self = Encoding.Unicode.GetString(decryptedBytes, 0, count);
        });
      });

      return self;
    }

    /// <summary>
    /// Encrypts the string value using Rijndael algorithm and converts the result to Base64 encoded string.
    /// <example><code>
    /// System.Console.WriteLine(
    ///   "Text to be encrypted.".Encrypt("secret")
    /// );
    /// Output: EfOSkt1p2yRO/TLxMJBcs/DeAa3MpMqkA1TMqZW/75czfOgGPqmTf6Nt1MsgRVHG
    /// </code></example>
    /// </summary>
    /// <param name="self">String to be encrypted.</param>
    /// <param name="password">Password used for encyption.</param>
    /// <returns>Encrypted text converted to Base64 encoded string.</returns>
    public static string Encrypt(this string self, string password) {
      new RijndaelManaged().Using(cipher => {
        PasswordDeriveBytes key = CreateDerivedKey(password);

        cipher.CreateEncryptor(key.GetBytes(32), key.GetBytes(16)).Using(encryptor => {
          new MemoryStream().Using(memoryStream => {
            try {
              new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write).Using(cryptoStream => {
                try {
                  byte[] plainBytes = Encoding.Unicode.GetBytes(self);

                  cryptoStream.Write(
                    plainBytes, 0, plainBytes.Length
                  );
                  cryptoStream.FlushFinalBlock();

                  self = Convert.ToBase64String(memoryStream.ToArray());
                } finally {
                  cryptoStream.Close();
                }
              });
            } finally {
              memoryStream.Close();
            }
          });
        });
      });

      return self;
    }

    static PasswordDeriveBytes CreateDerivedKey(string password) {
      return new PasswordDeriveBytes(
        password, Encoding.ASCII.GetBytes(password)
      );
    }

    /// <summary>
    /// Compares two string values, ignoring case considerations.
    /// </summary>
    /// <param name="self">This string.</param>
    /// <param name="s">Another string.</param>
    /// <returns>True if two strings are of the same length and corresponding characters in the two strings are equal ignoring case.</returns>
    public static bool EqualsIgnoreCase(this string self, string s) {
      if (s != null) {
        return self.ToLower().Equals(s.ToLower());
      }

      return false;
    }

    /// <summary>
    /// Tells whether or not the string <paramref name="self"/> matches the given regular expression pattern.
    /// </summary>
    /// <param name="self">This string.</param>
    /// <param name="pattern">The regex pattern to which the string of interest is to be matched.</param>
    /// <returns>True if the string matches the pattern.</returns>
    public static bool Matches(this string self, string pattern) {
      return Regex.IsMatch(self, pattern);
    }
    
    #endregion

    #region Uri extensions

    /// <summary>
    /// Downloads the resource addressed with URI <paramref name="self"/> and stores the content as a file.
    /// </summary>
    /// <param name="self">Uri to be downloaded.</param>
    /// <param name="localPath">Path to the local file.</param>
    /// <returns>FileInfo object of the created local file.</returns>
    public static FileInfo Download(this Uri self, string localPath) {
      if (!self.Scheme.StartsWith("http")) {
        throw new NotSupportedException(String.Format("Scheme '{0}' not supported.", self.Scheme));
      }
      FileInfo file = new FileInfo(localPath);

      WebRequest.Create(self).With(request => {
        ((HttpWebResponse)request.GetResponse()).Using(response => {
          try {
            response.GetResponseStream().Using(input => {
              new FileStream(file.FullName, FileMode.Create).Using(output => {
                input.CopyTo(output);
              });
            });
          } finally {
            response.Close();
          }
        });
      });

      return file;
    }

    #endregion
  }
}
